/*
 *  Copyright (c) 2011 Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the GNU Lesser Public License v2.1
 *  which accompanies this distribution, and is available at
 *  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 *  Contributors:
 *      Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany - initial API and implementation
 */
package de.ipk_gatersleben.bit.bi.edal.primary_data.file.implementation;

import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.lucene.index.IndexReader;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.criterion.Restrictions;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.SearchFactory;
import org.hibernate.search.reader.ReaderProvider;

import de.ipk_gatersleben.bit.bi.edal.primary_data.EdalConfiguration;
import de.ipk_gatersleben.bit.bi.edal.primary_data.metadata.implementation.MyUntypedData;

/**
 * IndexWriterThread class to realize manual indexing strategy
 * 
 * @author arendd
 */
public class IndexWriterThread extends Thread {

	private static final int SLEEP_RUNTIME_FACTOR = 2;

	private static final long MIN_THREAD_SLEEP = 500;

	private static final long MAX_THREAD_SLEEP = 2000;

	private SessionFactory sessionFactory;

	private int currentID = 0;

	private Logger threadLog = null;
	/** create Lock with fairness parameter true */
	private final ReentrantLock lock = new ReentrantLock(true);

	/**
	 * Constructor for IndexWriterThread
	 * 
	 * @param sessionFactory
	 *            the current {@link SessionFactory} object.
	 */
	protected IndexWriterThread(final SessionFactory sessionFactory) {

		DOMConfigurator.configure(EdalConfiguration.class
				.getResource("log4j.xml"));

		this.setThreadLog(Logger.getLogger("IndexWriterThread"));

		this.setSessionFactory(sessionFactory);

		final Session session = this.getSessionFactory().openSession();
		final SearchFactory searchFactory = Search.getFullTextSession(session)
				.getSearchFactory();

		final ReaderProvider readerProvider = searchFactory.getReaderProvider();

		final IndexReader reader = readerProvider.openReader(searchFactory
				.getDirectoryProviders(MyUntypedData.class));

		try {
			this.setCurrentID(reader.numDocs());
			this.getThreadLog().debug("START IndexWriterThread ...");
			this.getThreadLog().debug("Current docs : " + this.getCurrentID());
		} finally {
			readerProvider.closeReader(reader);
			session.close();
		}
	}

	private void executeIndexing() {

		if (!this.getSessionFactory().isClosed()) {
			final Session session = this.getSessionFactory().openSession();

			final FullTextSession fullTextSession = Search
					.getFullTextSession(session);

			/** high value fetch objects faster, but more memory is needed */
			final int batchSize = 500;

			fullTextSession.setFlushMode(FlushMode.MANUAL);
			fullTextSession.setCacheMode(CacheMode.NORMAL);
			final Transaction transaction = fullTextSession.beginTransaction();

			this.getThreadLog().debug("START Indexing...");

			/** ScrollableResults will avoid loading too many objects in memory */

			final long queryStartTime = System.currentTimeMillis();

			final ScrollableResults results = fullTextSession
					.createCriteria(MyUntypedData.class)
					.add(Restrictions.gt("id", this.getCurrentID()))
					.setFetchSize(batchSize).scroll(ScrollMode.FORWARD_ONLY);
			int index = 0;
			int countIndexedObjects = 0;

			final long queryTime = System.currentTimeMillis() - queryStartTime;

			final long indexStartTime = System.currentTimeMillis();
			while (results.next()) {
				index++;
				/** index each element */
				fullTextSession.index(results.get(0));
				if (index % batchSize == 0) {
					try {
						/** apply changes to indexes */
						fullTextSession.flushToIndexes();
						/** free memory since the queue is processed */
						fullTextSession.clear();
					} catch (Exception e) {
						throw new Error("Unable to read/write index files");
					}
				}
				countIndexedObjects++;
			}
			transaction.commit();
			session.close();

			this.setCurrentID(this.getCurrentID() + countIndexedObjects);

			final long indexingTime = System.currentTimeMillis()
					- indexStartTime;

			this.getThreadLog().debug(
					"INDEXING SUCCESSFUL : Objects|Index|Query : " + index
							+ " | " + indexingTime + " ms | " + queryTime
							+ " ms");

			try {
				Thread.sleep(Math.min(Math.max(indexingTime
						* IndexWriterThread.SLEEP_RUNTIME_FACTOR,
						IndexWriterThread.MIN_THREAD_SLEEP),
						IndexWriterThread.MAX_THREAD_SLEEP));
			} catch (final InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * @return the lock
	 */
	private ReentrantLock getLock() {
		return lock;
	}

	/**
	 * @return the currentID
	 */
	private int getCurrentID() {
		return this.currentID;
	}

	/**
	 * @return the sessionFactory
	 */
	private SessionFactory getSessionFactory() {
		return this.sessionFactory;
	}

	/**
	 * @return the threadLog
	 */
	private Logger getThreadLog() {
		return this.threadLog;
	}

	/** {@inheritDoc} */
	@Override
	public void run() {
		while (!this.getSessionFactory().isClosed()) {
			this.getThreadLog().debug("try lock run method");
			this.getLock().lock();
			this.getThreadLog().debug("locked run method");
			this.executeIndexing();
			this.getThreadLog().debug("unlock run method");
			this.getLock().unlock();
		}
//		if (this.getLock().isLocked()) {
//			this.getLock().unlock();
//		}
	}

	/**
	 * @param currentID
	 *            the currentID to set
	 */
	private void setCurrentID(final int currentID) {
		this.currentID = currentID;
	}

	/**
	 * @param sessionFactory
	 *            the sessionFactory to set
	 */
	private void setSessionFactory(final SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * @param threadLog
	 *            the threadLog to set
	 */
	private void setThreadLog(final Logger threadLog) {
		this.threadLog = threadLog;
	}

	/**
	 * wait until the indexing method is finished
	 */
	void waitForFinish() {

		final long time = System.currentTimeMillis();

		this.getThreadLog().debug("Wait for finish current indexing...");
		this.lock.lock();

		this.getThreadLog().debug("Got lock for last indexing...");

		this.getThreadLog().debug("FINALZE indexing...");
		this.executeIndexing();
		/** close SessionFactory so no indexing again */
		/** executeIndexing() runs only with open SessionFactory */
		this.getSessionFactory().close();
		this.lock.unlock();
		this.getThreadLog().debug(
				"Index is finished after waiting : "
						+ (System.currentTimeMillis() - time + " ms"));

		this.getThreadLog().debug("unlock Lock");
	}
}